/*
 *  Copyright 2017 Google Inc. All Rights Reserved.
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.google.blockly.ohos.ui;

import com.google.blockly.model.Block;
import com.google.blockly.model.BlocklyCategory;
import com.google.blockly.model.WorkspacePoint;
import com.google.blockly.ohos.ResourceTable;
import com.google.blockly.ohos.adapter.RecyclerAdapter;
import com.google.blockly.ohos.control.BlocklyController;
import com.google.blockly.ohos.control.ConnectionManager;
import com.google.blockly.utils.LogUtils;
import ohos.agp.components.*;
import ohos.agp.database.DataSetSubscriber;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.utils.Pair;

import java.util.ArrayList;
import java.util.List;

/**
 * Provides the standard configuration for a {@link }
 * to display a list of blocks, labels, and buttons.
 */

public class BlockRecyclerViewHelper {
    private static final String TAG = "BlockRVHelper";

    private final EventHandler mHandler = new EventHandler(EventRunner.getMainEventRunner());
    private final WorkspacePoint mTempWorkspacePoint = new WorkspacePoint();

    private final ListContainer mRecyclerView;
    private final Context mContext;
    private final LayoutScatter mHelium;
    private final Adapter mAdapter;
    private final CategoryCallback mCategoryCb;
    private final DirectionalLayoutManager mLayoutManager;

    private WorkspaceHelper mHelper;
    private ConnectionManager mConnectionManager;
    private FlyoutCallback mCallback;
    private BlocklyCategory mCurrentCategory;
    private BlockTouchHandler mTouchHandler;

    private static final int BLOCK_HEIGHT_PADDING = 10;

    public BlockRecyclerViewHelper(ListContainer recyclerView, final Context context) {
        mRecyclerView = recyclerView;
        mContext = context;
        mHelium = LayoutScatter.getInstance(mContext);
        mAdapter = new Adapter();
        mCategoryCb = new CategoryCallback();
        mLayoutManager = new DirectionalLayoutManager();

        mAdapter.addDataSubscriber(new DataSetSubscriber() {
            @Override
            public void onChanged() {
                super.onChanged();
                int height = 0;
                for (int i = 0; i < mAdapter.getItemCount(); i++) {
                    BlockViewHolder holder = (BlockViewHolder) mAdapter.onCreateViewHolder(null, mAdapter.getItemViewType(i));
                    try {
                        mAdapter.onBindViewHolder(holder, i);
                        holder.mContainer.estimateSize(Component.EstimateSpec.getSizeWithMode(0, Component.EstimateSpec.PRECISE),
                                Component.EstimateSpec.getSizeWithMode(0, Component.EstimateSpec.PRECISE));
                        if (holder.mContainer.getHeight() > height) {
                            height = holder.mContainer.getEstimatedHeight();
                        }
                        mAdapter.onViewRecycled(holder);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                mRecyclerView.setMinHeight(height + BLOCK_HEIGHT_PADDING);
            }
        });

        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setItemProvider(mAdapter);
//        mRecyclerView.addItemDecoration(new ItemSpacingDecoration(mAdapter));
    }

    /**
     * Initialize this helper. The controller and callback allows user interactions to
     * be handled correctly.
     *
     * @param controller The controller to use for view creation and drag handling.
     * @param callback   The {@link FlyoutCallback} that defines how the block list will respond to
     *                   user events.
     */
    public void init(BlocklyController controller, FlyoutCallback callback) {
        mCallback = callback;
        mHelper = controller.getWorkspaceHelper();
        mConnectionManager = controller.getWorkspace().getConnectionManager();

        mTouchHandler = controller.getDragger()
                .buildImmediateDragBlockTouchHandler(new DragHandler());
    }

    /**
     * Reset all the initialized components so this object may be attached to a new
     * controller or callback. The context and recycler view this was created with will
     * be retained.
     */
    public void reset() {
        mCallback = null;
        mHelper = null;
        mConnectionManager = null;
        mTouchHandler = null;
    }

    /**
     * Change the direction blocks should be laid out and scrolled in the RecyclerView.
     *
     * @param scrollOrientation
     */
    public void setScrollOrientation(int scrollOrientation) {
        mLayoutManager.setOrientation(scrollOrientation);
    }

    /**
     * Sets the category to connect to the {@link}.
     *
     * @param category The category to display blocks for.
     */
    public void setCurrentCategory(BlocklyCategory category) {
        if (mCurrentCategory == category) {
            return;
        }
        if (mCurrentCategory != null) {
            mCurrentCategory.setCallback(null);
        }
        mCurrentCategory = category;
        mRecyclerView.setItemProvider(mAdapter);
        mAdapter.notifyDataChanged();

        if (mCurrentCategory != null) {
            mCurrentCategory.setCallback(mCategoryCb);
        }
    }

    /**
     * @return The currently set category.
     */
    public BlocklyCategory getCurrentCategory() {
        return mCurrentCategory;
    }

    /**
     * Calculates the workspace point for a {@link PendingDrag}, such that the
     * {@link ohos.multimodalinput.event.TouchEvent#PRIMARY_POINT_DOWN} location remains in the same location on the screen
     * (i.e., under the user's finger), and calls
     * {@link FlyoutCallback#getDraggableBlockGroup} with the location. The workspace
     * point accounts for the {@link WorkspaceView}'s location, pan, and scale.
     *
     * @param pendingDrag The {@link PendingDrag} for the gesture.
     * @return The pair of {@link BlockGroup} and the view relative touch point returned by
     * {@link FlyoutCallback#getDraggableBlockGroup}.
     */
    private Pair<BlockGroup, ViewPoint> getWorkspaceBlockGroupForTouch(PendingDrag pendingDrag) {
        BlockView touchedBlockView = pendingDrag.getTouchedBlockView();
        Block rootBlock = touchedBlockView.getBlock().getRootBlock();
        BlockView rootTouchedBlockView = mHelper.getView(rootBlock);
        BlockGroup rootTouchedGroup = rootTouchedBlockView.getParentBlockGroup();

        // Calculate the offset from rootTouchedGroup to touchedBlockView in view
        // pixels. We are assuming the only transforms between BlockViews are the
        // child offsets.
        Component view = (Component) touchedBlockView;
        float offsetX = view.getContentPositionX() + pendingDrag.getTouchDownViewOffsetX();
        float offsetY = view.getContentPositionY() + pendingDrag.getTouchDownViewOffsetY();
        ComponentContainer parent = (ComponentContainer) view.getComponentParent();
        while (parent != rootTouchedGroup) {
            view = parent;
            offsetX += view.getContentPositionX();
            offsetY += view.getContentPositionY();
            parent = (ComponentContainer) view.getComponentParent();
        }
        ViewPoint touchOffset = new ViewPoint((int) Math.ceil(offsetX),
                (int) Math.ceil(offsetY));

        // Adjust for RTL, where the block workspace coordinate will be in the top right
        if (mHelper.useRtl()) {
            offsetX = rootTouchedGroup.getWidth() - offsetX;
        }

        // Scale into workspace coordinates.
        int wsOffsetX = mHelper.virtualViewToWorkspaceUnits(offsetX);
        int wsOffsetY = mHelper.virtualViewToWorkspaceUnits(offsetY);

        // Offset the workspace coord by the BlockGroup's touch offset.
        mTempWorkspacePoint.setFrom(
                pendingDrag.getTouchDownWorkspaceCoordinates());
        mTempWorkspacePoint.translate(-wsOffsetX, -wsOffsetY);

        int itemIndex = mCurrentCategory.indexOf(rootBlock); // Should never be -1
        BlockGroup dragGroup = mCallback.getDraggableBlockGroup(itemIndex, rootBlock,
                mTempWorkspacePoint);
        return Pair.create(dragGroup, touchOffset);
    }

    /**
     * Internal implementation that listens to changes to the category and refreshes
     * the recycler view if it changes.
     */
    protected class CategoryCallback extends BlocklyCategory.Callback {

        @Override
        public void onItemAdded(int index, BlocklyCategory.CategoryItem item) {
            mAdapter.notifyDataSetItemInserted(index);
        }

        @Override
        public void onItemRemoved(int index, BlocklyCategory.CategoryItem block) {
            mAdapter.notifyDataSetItemRemoved(index);
        }

        @Override
        public void onCategoryCleared() {
            mAdapter.notifyDataChanged();
        }
    }

    /**
     * Adapts {@link Block}s in list into {@link BlockGroup}s inside {@Link FrameLayout}.
     */

    public class Adapter extends RecyclerAdapter<BlockViewHolder> {

        @Override
        public ViewHolder onCreateViewHolder(ComponentContainer parent, int viewType) {
            return new BlockViewHolder(mContext);
        }

        @Override
        public int getItemCount() {
            return mCurrentCategory == null ? 0 : mCurrentCategory.getItems().size();
        }

        @Override
        public int getItemViewType(int position) {
            if (mCurrentCategory == null) {
                return -1;
            }
            return mCurrentCategory.getItems().get(position).getType();
        }

        @Override
        public void onBindViewHolder(BlockViewHolder holder, int position) throws Exception {
            List<BlocklyCategory.CategoryItem> items = mCurrentCategory == null
                    ? new ArrayList<BlocklyCategory.CategoryItem>()
                    : mCurrentCategory.getItems();
            BlocklyCategory.CategoryItem item = items.get(position);
            if (item.getType() == BlocklyCategory.CategoryItem.TYPE_BLOCK) {
                Block block = ((BlocklyCategory.BlockItem) item).getBlock();
                BlockGroup bg = mHelper.getParentBlockGroup(block);
                if (bg == null) {
                    bg = mHelper.getBlockViewFactory().buildBlockGroupTree(
                            block, mConnectionManager, mTouchHandler);
                } else {
                    bg.setTouchHandler(mTouchHandler);
                }

                if (bg.getComponentParent() != null) {
                    bg.getComponentParent().removeComponent(bg);
                }

                holder.mContainer.addComponent(bg, new StackLayout.LayoutConfig(
                        DependentLayout.LayoutConfig.MATCH_CONTENT,
                        DependentLayout.LayoutConfig.MATCH_CONTENT));
                holder.bg = bg;
            } else if (item.getType() == BlocklyCategory.CategoryItem.TYPE_BUTTON) {
                BlocklyCategory.ButtonItem bItem = (BlocklyCategory.ButtonItem) item;
                final String action = bItem.getAction();
                Button button = (Button) mHelium.parse(ResourceTable.Layout_simple_button, holder.mContainer,
                        false);
                holder.mContainer.addComponent(button);
                button.setText(bItem.getText());
                button.setClickedListener(v -> {
                    if (mCallback != null) {
                        mCallback.onButtonClicked(v, action, mCurrentCategory);
                    }
                });
            } else if (item.getType() == BlocklyCategory.CategoryItem.TYPE_LABEL) {
                BlocklyCategory.LabelItem lItem = (BlocklyCategory.LabelItem) item;
                Text label = (Text) mHelium.parse(ResourceTable.Layout_simple_label,
                        holder.mContainer, false);
                holder.mContainer.addComponent(label);
                label.setText(lItem.getText());
            } else {
                LogUtils.e(TAG, "Tried to bind unknown item type " + item.getType());
            }
        }

        @Override
        public Object getItem(int i) {
            return mCurrentCategory.getItems().get(i);
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        @Override
        public void notifyDataChanged() {
            super.notifyDataChanged();
            LogUtils.i("BlockRecyclerViewHelper", "notifyDataChanged mCurrentCategory:" + mCurrentCategory);
        }

        public void onViewRecycled(BlockViewHolder holder) {
            // If this was a block item BlockGroup may be reused under a new parent.
            // Only clear if it is still a child of mContainer.
            if (holder.bg != null && holder.bg.getComponentParent() == holder.mContainer) {
                holder.bg.unlinkModel();
                holder.bg = null;
                holder.mContainer.removeAllComponents();
            }
        }
    }

    private class BlockViewHolder extends RecyclerAdapter.ViewHolder {
        final StackLayout mContainer;
        BlockGroup bg = null;  // Root of the currently attach block views.

        BlockViewHolder(Context context) {
            super(new StackLayout(context));
            mContainer = (StackLayout) itemView;
        }
    }

    /**
     * {@link Dragger.DragHandler} implementation for BlockListViews.
     */
    private class DragHandler implements Dragger.DragHandler {

        @Override
        public Runnable maybeGetDragGroupCreator(final PendingDrag pendingDrag) {
            return new Runnable() {
                @Override
                public void run() {
                    // Acquire the draggable BlockGroup on the Workspace from the
                    // {@link OnDragListBlock}.
                    Pair<BlockGroup, ViewPoint> dragGroupAndTouchOffset =
                            getWorkspaceBlockGroupForTouch(pendingDrag);
                    if (dragGroupAndTouchOffset != null) {
                        pendingDrag.startDrag(
                                mRecyclerView,
                                dragGroupAndTouchOffset.f,
                                dragGroupAndTouchOffset.s);
                    }

                }
            };
        }

        @Override
        public boolean onBlockClicked(final PendingDrag pendingDrag) {
            mHandler.postTask(new Runnable() {
                @Override
                public void run() {
                    // Identify and process the clicked BlockGroup.
                    getWorkspaceBlockGroupForTouch(pendingDrag);
                }
            });
            return true;
        }
    }

}
